<?php
/**
 * @file 3gplex/includes/taxonomy.inc
 * 
 * Taxonomy core module configuration API.
 * 
 */
require_once('./'. drupal_get_path('module', 'drupalplex') .'/includes/module.inc');



define("PTAXO_NO_SUCH_FILE", "Unable to read or find definition of vocabulary %vocabulary");
define("PTAXO_ADD_VOCABULARY_FAILED", "Can't create vocabulary %vocabulary:
	Call to taxonomy_save_vocabulary failed (maybe vocabulary already exist?)");
define("PTAXO_VOCABULARY_EXISTS", "Can't create vocabulary %vocabulary:
	This vocabulary already exists");
define("PTAXO_NO_CATEGORY_NAME", "Can't create some term to vocabulary %vocabulary:
	Term to add has no name");
define("PTAXO_INVALID_HIERARCHY", "Can't create vocabulary %vocabulary:
	It has invalid (neither disabled, single nor multiple) hierarchy setting");

define("PTAXO_NO_CATEGORY", "Can't add some term to vocabulary %vocabulary:
	It refers to term %category (as a parent or related term) which is not defined");
define("PTAXO_ADD_CATEGORY_FAILED", "Can't add term %category to vocabulary %vocabulary:
	Call to taxonomy_term_save failed mysteriously");
define("PTAXO_INVALID_RELATION", "Can't add term %category to vocabulary %vocabulary:
	One of it's related terms is not defined");
define("PTAXO_INVALID_PARENT", "Can't add term %category to vocabulary %vocabulary:
	One of it's parent terms is not defined");
define("PTAXO_INFINITE_LOOP", "Can't add term %category to vocabulary %vocabulary:
	I detected a loop (one of it's references refers back to it)");
define("PTAXO_NO_VOCABULARY", "Can't add term %category to vocabulary %vocabulary:
	Vocabulary does not exist");

class plex_taxonomy_vocabulary_exception extends plex_exception
{
	function __construct($message, $vocabulary)
	{
		$this->message = $message;
		$this->arguments = array('%vocabulary'=>$vocabulary);
	}
}

class plex_taxonomy_category_exception extends plex_taxonomy_vocabulary_exception
{
	function __construct($message, $vocabulary, $category)
	{
		$this->message = $message;
		$this->arguments = array('%vocabulary'=>$vocabulary, '%category'=>$category);
	}
}


/**
 * Taxonomy core module wrapper.
 * 
 * This module ensures users can categorize a drupal site content and set the categories.
 * It provides remarquably well done functions to reuse.
 * 
 * As vocabularies can be quite big and it wouldn't be practical to all define them in
 * one file configuration is split into global configuration and one file per vocabulary
 * you want to add.
 *
 * @todo Add a static cache of vocabulary's ids
 */
class plex_taxonomy extends plex_module
{
	const description = "Configures and loads API for Drupal core taxonomy module: Categorization of your content";
	function get_description() { return self::description; }
	protected $suggested = array( 'pathauto' );
	/** Vocabulary is not hierarchic. */
	const hier_disabled = 0;
	/** Vocabulary features hierarchy with one parent. */
	const hier_single = 1;
	/** Vocabulary features hierarchy with multiple parents. */
	const hier_multiple = 2;
	/** Default directory to look in for vocabulary statements xml files. */
	static $default_vocabulary_dir = null;

	protected function revert_settings(array $settings)
	{
		foreach ($settings as $vocabulary => $properties) {
			$this->rem_vocabulary($vocabulary);
		}
	}
	
	protected function build_settings_xml($xml)
	{
		$settings = array();
		$vocabularies = array();
		if (isset($xml->install_all) && $xml->install_all == true) {
			$files = file_scan_directory(self::$default_vocabulary_dir, '\.xml$', array('.', '..', 'CVS'), 0, true, 'filename');
			foreach ($files as $path => $file) {
				$vocabularies[] = basename($path, '.xml');
			}
		} else foreach ($xml->vocabularies->vocabulary as $vocabulary) {
			$vocabularies[] = (string)$vocabulary;
		}

		foreach ($vocabularies as $vocabulary) {
			$vxml = simplexml_load_file(self::$default_vocabulary_dir."/{$vocabulary}.xml");
			if ($vxml === false) throw new plex_taxonomy_vocabulary_exception(PTAXO_NO_SUCH_FILE, $vocabulary);
			$settings[$vocabulary] = $this->build_vocabulary_xml($vocabulary, $vxml);
		}
		return $settings;
	}

	protected function build_vocabulary_xml($name, $xml)
	{
		$v = array(
			'description' => isset($xml->description) ? (string)$xml->description : "",
			'help' => isset($xml->help) ? (string)$xml->help : "",
			'multiple' => isset($xml->multiple) ? (int)$xml->multiple : 1,
			'required' => isset($xml->required) ? (int)$xml->required : 0,
			'hierarchy' => isset($xml->hierarchy) ? (string)$xml->hierarchy : 'single',
			'relations' => isset($xml->relations) ? (int)$xml->relations : 1,
			'tags' => isset($xml->tags) ? (int)$xml->tags : 0,
			'weight' => isset($xml->weight) ? (int)$xml->weight : 1,
			'categories' => array(),
		);
		if (isset($xml->module)) {
			$v['module'] = (string)$xml->module;
		}
		if (isset($xml->nodes->node)) {
			foreach ($xml->nodes->node as $node) {
				$v['nodes'][] = (string)$node;
			}
		}
		if (isset($xml->pattern)) {
			$v['pattern'] = (string)$xml->pattern;
		}
		foreach ($xml->category as $category) {
			$category_name = (string)$category->name;
			if (strlen($category_name) < 1)
				throw new plex_taxonomy_vocabulary_exception(PTAXO_NO_CATEGORY_NAME, $name);
			$v['categories'][$category_name] = $this->build_category_xml($category);
		}
		return $v;
	}

	protected function build_category_xml($xml)
	{
		$c = array(
			'description' => isset($xml->description) ? (string)$xml->description : "",
			'weight' => isset($xml->weight) ? (int)$xml->weight : 0,
			'relations' => array(),
			'parent' => array(),
			'synonyms' => array()
		);
		if (isset($xml->relations->relation)) {
			foreach ($xml->relations->relation as $name) {
				$c['relations'][] = (string)$name;
			}
		}
		if (isset($xml->parents->parent)) {
			foreach ($xml->parents->parent as $name) {
				$c['parent'][] = (string)$name;
			}
		}
		if (isset($xml->synonyms->synonym)) {
			foreach ($xml->synonyms->synonym as $name) {
				$c['synonyms'][] = (string)$name;
			}
		}
		return $c;
	}

	protected function validate_settings(array &$settings)
	{
		foreach ($settings as $name => $properties) {
			$this->validate_vocabulary_settings($name, $settings[$name]);
		}
	}

	protected function validate_vocabulary_settings($vocabulary, &$settings)
	{
		if ($this->vocabulary_exists($vocabulary)) {
			throw new plex_taxonomy_vocabulary_exception(PTAXO_VOCABULARY_EXISTS, $vocabulary);
		}
		$settings['name'] = $vocabulary;
		switch ($settings['hierarchy']) {
			case 'disabled': $settings['hierarchy'] = self::hier_disabled; break;
			case 'single': $settings['hierarchy'] = self::hier_single; break;
			case 'multiple': $settings['hierarchy'] = self::hier_multiple; break;
			default:
			throw new plex_taxonomy_vocabulary_exception(PTAXO_INVALID_HIERARCHY, $vocabulary);
		}
		foreach ($settings['categories'] as $name => $properties) {
			$this->validate_category_settings($vocabulary, $settings['categories'], "$name");
		}
	}
	
	protected function validate_category_settings($vocabulary, array &$categories, $name)
	{
		if (!isset($categories["$name"]))
			throw new plex_taxonomy_category_exception(PTAXO_NO_CATEGORY, $vocabulary, $name);
		if (isset($categories["$name"]['__mark'])) {
			if ($categories["$name"]['__mark'] === true) return true;
			throw new plex_taxonomy_category_exception(PTAXO_INFINITE_LOOP, $vocabulary, $name);
		}
		$categories["$name"]['__mark'] = false;
		$categories["$name"]['name'] = $name;
		
		foreach ($categories["$name"]['relations'] as $relation) {
			if (!isset($categories["$relation"])) {
				throw new plex_taxonomy_category_exception(PTAXO_INVALID_RELATION, $vocabulary, $name);
			}
			$this->validate_category_settings($vocabulary, $categories, $relation);
		}
		foreach ($categories["$name"]['parent'] as $parent) {
			if (!isset($categories["$parent"])) {
				throw new plex_category_exception(PTAXO_INVALID_PARENT, $vocabulary, $name);
			}
			$this->validate_category_settings($vocabulary, $categories, $parent);
		}
		$synonyms = "";
		foreach ($categories["$name"]['synonyms'] as $synonym) {
			$synonyms .= $synonym."\n";
		}
		$categories["$name"]['synonyms'] = $synonyms;
		$categories["$name"]['__mark'] = true;
	}

	protected function apply_settings(array $settings)
	{
		foreach ($settings as $name => $vocabulary) {
			$this->apply_vocabulary_settings($name, $vocabulary);
		}
	}

	protected function apply_vocabulary_settings($name, $vocabulary)
	{
		$status = taxonomy_save_vocabulary($vocabulary);
		if ($status != SAVED_NEW)
			throw new plex_taxonomy_vocabulary_exception(PTAXO_ADD_VOCABULARY_FAILED, $name);
		//
		// Now that placeholder is created set pathauto pattern if any
		if (isset($vocabulary['pattern'])) {
			$modules = plex_modules::get_instance();
			$modules->load('pathauto');
			$modules->pathauto->set_taxonomy_pattern($vocabulary['vid'], $vocabulary['pattern']);
		}
		//
		// This array will have categories names as index and their tid as value, it is necessary for relations and hierarchy
		foreach ($vocabulary['categories'] as $category => $properties) {
			$this->validate_category_settings($vocabulary['name'], $vocabulary['categories'], $category);
			$this->apply_category_settings($vocabulary['name'], $vocabulary['categories'], $category);
		}
		plex::notice("Created new vocabulary %name", array('%name' => $name));
	}

	protected function apply_category_settings($vocabulary, array &$categories, $name)
	{
		if (!isset($categories["$name"]['__mark']) || !$categories["$name"]['__mark'])
			throw new plex_debug_exception;
		$vid = $this->name2id($vocabulary);
		if ($vid === null)
			throw new plex_taxonomy_category_exception(PTAXO_NO_VOCABULARY, $vocabulary, $name);
		$categories["$name"]['vid'] = $vid;
		$relations = array();
		foreach ($categories["$name"]['relations'] as $relation) {
			if (!isset($categories["$relation"]['tid'])) {
				if (!isset($categories["$relation"])) {
					throw new plex_debug_exception();
				}
				$this->apply_category_settings($vocabulary, $categories, $relation);
			}
			$relations[] = $categories["$relation"]['tid'];
		}
		$categories["$name"]['relations'] = $relations;

		$parents = array();
		foreach ($categories["$name"]['parent'] as $parent) {
			if (!isset($categories["$parent"]['tid'])) {
				$this->apply_category_settings($vocabulary, $categories, $parent);
			}
			$parents[] = $categories["$parent"]['tid'];
		}
		$categories["$name"]['parent'] = $parents;
		
		$status = taxonomy_save_term($categories["$name"]);
		if ($status !== SAVED_NEW)
			throw new plex_taxonomy_category_exception(PTAXO_ADD_CATEGORY_FAILED, $vocabulary, $name);
		plex::notice("Created new term %term for vocabulary %vocabulary", array('%term'=>$name, '%vocabulary'=>$vocabulary));
	}
	/**
	 * Given an xml representation of non-already installed drupal vocabulary, install it. 
	 */
	function add_vocabulary($name, $vocabulary)
	{ try {
		$this->validate_vocabulary_settings($name, $vocabulary);
		$this->apply_vocabulary_settings($name, $vocabulary);
		return true;
	} catch (plex_exception $err) {
		$err->display();
		return false;
	} }

	/**
	 * For each term: save back drupal generated id, replace names in reference tables (hierarchy,relation) by corresponding ids.
	 * 
	 * @todo if refmap is null then fetch parent and related category ids using taxonomy module functions.
	 */
	function add_category($vocabulary, array &$categories, $name)
	{ try {
		$this->validate_category_settings($vocabulary, $categories, $name);
		$this->apply_category_settings($vocabulary, $categories, $name);
		plex::notice("Created new term %name with tid %tid",
			array('%name'=>$settings['name'],'%tid'=>$settings['tid']));
		return true;
	} catch (plex_exception $err) {
		$err->display();
		return false;
	} }
	
	/**
	 * Given an xml description file attempts to create the described vocabulary.
	 */
	function add_vocabulary_xml($path)
	{
		if (file_exists($path)) $name = basename($path, '.xml');
		else { $name = $path; $path = self::$default_vocabulary_dir."/$name.xml"; }
		$statements = simplexml_load_file($path);
		if ($statements === false) {
			plex::warn("Can't install vocabulary %name to web site: Unable to read %path xml file",
			array('%name'=>$name, '%path'=>$path));
			return false;
		}
		$settings = $this->build_vocabulary_xml($name, $statements);
		$this->validate_vocabulary_settings();
		return $this->add_vocabulary($name, $statements);
	}

	/**
	 * Browse a directory for xml description files and attempts to create described vocabularies.
	 */
	function add_vocabulary_dir($dir = null)
	{
		if ($dir === null) $dir = self::$default_vocabulary_dir;

		$vocabularies = file_scan_directory($dir, '\.xml$', array('.', '..', 'CVS'), 0, true, 'filename');
	
		foreach ($vocabularies as $path => $file) {
			$this->add_vocabulary_xml($path);
		}
	}

	/**
	 * Returns the vid for vocabulary of given name or -1 if inexistent.
	 */
	function name2id($name)
	{
		$res = db_query("select vid from {vocabulary} where name='%s'", $name);
		if ($res === false || db_num_rows($res) !== 1) return null;
		return db_result($res);
	}

	/**
	 * Returns true if vocabulary of given name exists.
	 */
	function vocabulary_exists($name)
	{
		$res = db_query("select 1 from {vocabulary} where name='%s'", $name);
		if ($res === false || db_num_rows($res) === 0) return false;
		return true;
	}

	/**
	 * Returns the name for vocabulary of given vid or null if inexistent.
	 */
	function id2name($id)
	{
		$res = db_query("select name from {vocabulary} where vid=%d", $id);
		if ($res === false || db_num_rows($res) !== 1) return null;
		return db_result($res);
	}

	/**
	 * Given a SimpleXMLElement describing a vocabulary or any object with corresponding properties attempts
	 * to delete corresponding vocabulary.
	 */
	function rem_vocabulary($vocabulary)
	{
		$vid = $this->name2id($vocabulary);
		if ($vid === null) {
			plex::warn("plex_taxonomy::rem_vocabulary(): Vocabulary '%name' doesn't exist",
			array('%name'=>$vocabulary));
			return false;
		}
		taxonomy_del_vocabulary($vid);
		return true;
	}

	/**
	 * Given an xml description file attempts to delete the described vocabulary.
	 */
	function rem_vocabulary_xml($string)
	{
		if (file_exists($string)) $vocabulary = basename($string, '.xml');
		else $vocabulary = $string;
		return $this->rem_vocabulary($vocabulary);
	}

	/**
	 * Browse a directory for xml description files and attempts to delete described vocabularies.
	 */
	function rem_vocabulary_dir($dir = null)
	{
		if ($dir === null) $dir = self::$default_vocabulary_dir;

		$vocabularies = file_scan_directory($dir, '\.xml$', array('.', '..', 'CVS'), 0, true, 'filename');
	
		foreach ($vocabularies as $path => $file) {
			$this->rem_vocabulary_file($path);
		}
	}
}

/*
 * Fine.
 */
plex_taxonomy::$default_vocabulary_dir = "./".drupal_get_path('module', 'drupalplex')."/vocabularies";
/**
 * @page plex_taxonomy_page Taxonomy core module configuration formats
 * 
 * Drupal documentation for taxonomy: http://drupal.org/node/299 @n
 * Taxonomy base API: http://api.drupal.org/api/file/modules/taxonomy/taxonomy.module/5
 *
 * @section plex_taxonomy_data_types Node Data Types
 * 
 * Terminology: http://drupal.org/getting-started/before/terminology#taxonomy @n
 * I would like to add that plex does use the word 'category' to refer to terms.
 * 
 * @section plex_taxonomy_vocabularies Vocabularies
 * 
 * When writing a vocabulary it is important to order the terms description so that any
 * parents of them is already defined. To help ensure this we recommend separate terms
 * in the configuration file by their level in hierarchy (inside vocabulary).
 * 
 * We even recommend to consider for a term it's level in hierarchy to be the one below
 * the level of it's parent with the lowest level.
 *
 * @section plex_taxonomy_examples Full Configuration Samples
 * 
 * @subsection plex_taxonomy_vocabulary_example Vocabulary Description
 * 
 * @verbinclude "Field of Interest.xml"
 * 
 */
